import { camelCase, identity, kebabCase, mapValues, merge } from "lodash-es";
import { ExpString, VNode, VNodeLike, VNodeSymbol } from "@/types";
import { isBooleanLiteral, isStringLiteral, stringifyExp } from "@/utils";

type Tag = string;
export function createVNode(vnodeLike: VNodeLike | VNode | Tag): VNode {
  if (typeof vnodeLike === "string")
    return createVNode({
      tag: vnodeLike,
    });

  if (VNodeSymbol in vnodeLike) return vnodeLike as VNode;

  const { tag, props = {}, children = [] } = vnodeLike;
  const vnode: VNode = {
    tag,
    props: mapValues(props, stringifyExp),
    children: children.map((child) => createVNode(child)),
    [VNodeSymbol]: true,
  };
  return vnode;
}

// 处理Prop时的上下文对象
interface PropOpContext {
  prop: string;
  value: ExpString;
  vnode: VNode;
}

interface VNodeToStringOptions {
  tag?: {
    selfClosing?: boolean;
    format?: (tag: string, vnode: VNode) => string;
  };
  property?: {
    sort?: (a: PropOpContext, b: PropOpContext) => number;
    format?: (ctx: PropOpContext) => string;
  };
  tabs?: number;
}

export function vnodeToString(vnode: VNode, options?: VNodeToStringOptions) {
  const defaultOptions: DeepRequiredObject<VNodeToStringOptions> = {
    tag: {
      selfClosing: false,
      format: identity,
    },
    property: {
      sort: () => 0,
      format: ({ prop, value }) => `${prop}="${value}"`,
    },
    tabs: 2,
  };
  options = merge(defaultOptions, options || {});

  /** 控制缩进 */
  let deepth = -1;

  function __vnodeToString(
    vnode: VNode,
    options: DeepRequiredObject<VNodeToStringOptions>
  ): string {
    deepth++;

    let propsStr = Object.keys(vnode.props)
      .sort((prop1, prop2) => {
        const value1 = vnode.props[prop1];
        const value2 = vnode.props[prop2];
        return options.property.sort(
          {
            prop: prop1,
            value: value1,
            vnode,
          },
          {
            prop: prop2,
            value: value2,
            vnode,
          }
        );
      })
      .map((prop) => {
        const value = vnode.props[prop];
        return options.property.format({
          prop,
          value,
          vnode,
        });
      })
      .join(" ");

    const tag = options.tag.format(vnode.tag, vnode);
    let childrenStr = vnode.children
      .map((vnode) => __vnodeToString(vnode, options))
      .join("\n");

    const space = " ".repeat(deepth * options.tabs);
    const selfClosing = options.tag.selfClosing && vnode.children.length === 0;
    if (selfClosing && propsStr) propsStr += " ";
    if (!selfClosing && propsStr) propsStr = ` ${propsStr}`;

    if (childrenStr) childrenStr = `\n${childrenStr}\n${space}`;

    const str = selfClosing
      ? `${space}<${tag} ${propsStr}/>`
      : `${space}<${tag}${propsStr}>${childrenStr}</${tag}>`;

    deepth--;
    return str;
  }

  return __vnodeToString(
    vnode,
    options as DeepRequiredObject<VNodeToStringOptions>
  );
}

type VNodePropFormatter =
  DeepRequiredObject<VNodeToStringOptions>["property"]["format"];

const JSXPropFormatter: VNodePropFormatter = ({ prop, value }) => {
  if (prop !== "v-model") {
    prop = camelCase(prop);
  }
  if (isBooleanLiteral(value) && value === "true") return prop;
  const valueIsString = isStringLiteral(value);
  const delimiters = [valueIsString ? '"' : "{", valueIsString ? '"' : "}"];
  if (valueIsString) {
    value = value.slice(1, value.length - 1);
  }
  const [s, e] = delimiters;
  return `${prop}=${s}${value}${e}`;
};

const VueTemplatePropFormatter: VNodePropFormatter = ({ prop, value }) => {
  if (prop !== "value.sync") {
    prop = kebabCase(prop);
  }
  if (isBooleanLiteral(value) && value === "true") return prop;
  const valueIsString = isStringLiteral(value);
  if (valueIsString) {
    value = value.slice(1, value.length - 1);
  }
  const prefix = prop === "v-model" || valueIsString ? "" : ":";
  return `${prefix}${prop}="${value}"`;
};

/** 内置的prop-formatter */
vnodeToString.propFormatters = {
  JSXPropFormatter,
  VueTemplatePropFormatter,
};
